package protocol

import (
	"fmt"
	"strings"
	"sync"
)

// GitCommand contains the very basic information about a git command
type GitCommand struct {
	OldOID string
	NewOID string
	Ref    string
}

func (c GitCommand) String() string {
	return strings.Join([]string{c.OldOID, c.NewOID, c.Ref}, " ")
}

// CommandStatus is the status of a command after proc-receive processing, currently
// only "ok" and "ng" supported
type CommandStatus int

const (
	// CommandStatusOK ok
	CommandStatusOK CommandStatus = iota
	// CommandStatusNG ng
	CommandStatusNG
)

// Command is designed for proc-receive to support agit extended features(spacex, agitflow...)
type Command struct {
	sync.RWMutex
	// raw command received from git-receive-pack
	GitCommand
	// intented command may differ from what actually got during proc-receive processing
	intended GitCommand
	// the status of the command, `ok` or `ng`
	status CommandStatus
	// fail reason when status is `ng`
	reason string
	// reports used to inform user about what magic happened to the command
	hints string
}

// NewCommand creates a new command with the given command as `intended`
// use `SetIntendedCommand` if you want to change the intended command later
func NewCommand(cmd GitCommand) (*Command, error) {
	if cmd.Ref == "" || cmd.NewOID == "" || cmd.OldOID == "" {
		return nil, fmt.Errorf("refname, old-oid, new-oid must not be empty")
	}

	return &Command{
		GitCommand: cmd,
		intended:   cmd, // set intended command the same as original when initializing
	}, nil
}

// GetIntendedCommand returns the intended command currently
func (c *Command) GetIntendedCommand() GitCommand {
	c.Lock()
	defer c.Unlock()

	return c.intended
}

// SetIntendedCommand overwrites the intended command using the given command
func (c *Command) SetIntendedCommand(cmd GitCommand) error {
	c.Lock()
	defer c.Unlock()

	if cmd.Ref == "" || cmd.NewOID == "" || cmd.OldOID == "" {
		return fmt.Errorf("refname, old-oid, new-oid must not be empty")
	}

	c.intended = cmd
	return nil
}

// SetStatus sets status and reason of the command, caller should provide the reason when `ng`
func (c *Command) SetStatus(status CommandStatus, reason string) {
	c.Lock()
	defer c.Unlock()

	c.status = status
	c.reason = reason
}

// SetStatusNG sets command status to NG with the given reason
func (c *Command) SetStatusNG(reason string) {
	c.Lock()
	defer c.Unlock()

	c.status = CommandStatusNG
	if c.reason == "" {
		c.reason = reason
	}
}

// SetHint sets hint for the command, the hint would be reported to the user later
func (c *Command) SetHint(h string) {
	c.Lock()
	defer c.Unlock()
	if c.hints == "" {
		c.hints = h
	}
}

// GenerateReports generates reports for git-receive-pack
func (c *Command) GenerateReports() []string {
	c.RLock()
	defer c.RUnlock()

	var reports []string
	switch c.status {
	case CommandStatusOK:
		reports = append(reports, fmt.Sprintf("ok %s", c.Ref))
		if c.Ref != c.intended.Ref {
			reports = append(reports, fmt.Sprintf("option refname %s", c.intended.Ref))
		}
		reports = append(reports,
			fmt.Sprintf("option old-oid %s", c.intended.OldOID),
			fmt.Sprintf("option new-oid %s", c.intended.NewOID),
		)
	case CommandStatusNG:
		reason := strings.TrimSpace(c.reason)
		if reason == "" {
			reason = "unknown"
		}
		reports = append(reports, fmt.Sprintf("ng %s %s", c.Ref, reason))
	}

	return reports
}

// GetHint returns the hint for the command
func (c *Command) GetHint() string {
	c.RLock()
	defer c.RUnlock()

	return c.hints
}

// GetReason returns the reason for the command
func (c *Command) GetReason() string {
	c.RLock()
	defer c.RUnlock()

	return c.reason
}

func (c *Command) GetStatus() CommandStatus {
	return c.status
}
